Skip to content

命令式弹窗组件

在 Vue 中实现命令式弹窗组件(如 this.$dialog.show(args)),核心思路是通过 动态创建组件实例 并挂载到 DOM 上,结合配置项实现灵活的弹窗控制。以下是完整实现方案,支持默认确认框、自定义组件传入及丰富的配置项:

一、实现思路

  1. 基础结构:创建一个弹窗容器组件(DialogContainer),负责渲染默认确认框或自定义组件。
  2. 命令式 API:封装 $dialog 方法(如 showclose),通过 Vue.extendcreateVNode 动态创建组件实例。
  3. 配置系统:支持传入标题、内容、按钮文本、关闭逻辑等配置项。
  4. 组件通信:通过回调函数或 Promise 处理弹窗的确认/取消事件。

二、完整实现代码

1. 弹窗容器组件(DialogContainer.vue

vue
<template>
  <div class="dialog-mask" @click="handleMaskClose">
    <div class="dialog-wrapper" :style="dialogStyle">
      <!-- 关闭按钮 -->
      <button v-if="config.showClose" class="dialog-close" @click="handleClose">
        ×
      </button>

      <!-- 标题 -->
      <div v-if="config.title" class="dialog-title">{{ config.title }}</div>

      <!-- 内容区域:默认文本/自定义组件 -->
      <div class="dialog-content">
        <!-- 自定义组件 -->
        <component
          v-if="config.component"
          :is="config.component"
          v-bind="config.props || {}"
          v-on="config.events || {}"
        />
        <!-- 默认文本内容 -->
        <div v-else>{{ config.content }}</div>
      </div>

      <!-- 按钮区域 -->
      <div class="dialog-footer" v-if="!config.hideFooter">
        <button class="dialog-btn cancel" @click="handleCancel">
          {{ config.cancelText || "取消" }}
        </button>
        <button class="dialog-btn confirm" @click="handleConfirm">
          {{ config.confirmText || "确认" }}
        </button>
      </div>
    </div>
  </div>
</template>

<script setup>
  import { defineProps, emit } from "vue";

  // 定义配置项类型
  const props = defineProps({
    config: {
      type: Object,
      default: () => ({
        title: "", // 标题
        content: "", // 默认文本内容
        component: null, // 自定义组件
        props: null, // 自定义组件的props
        events: null, // 自定义组件的事件
        showClose: true, // 是否显示关闭按钮
        hideFooter: false, // 是否隐藏底部按钮
        cancelText: "取消", // 取消按钮文本
        confirmText: "确认", // 确认按钮文本
        closeOnMask: true, // 点击遮罩是否关闭
        closeOnConfirm: true, // 点击确认后是否关闭
      }),
    },
  });

  const emit = defineEmits(["confirm", "cancel", "close"]);

  // 点击遮罩关闭
  const handleMaskClose = () => {
    if (props.config.closeOnMask) {
      emit("close");
    }
  };

  // 点击关闭按钮
  const handleClose = () => emit("close");

  // 取消按钮
  const handleCancel = () => {
    emit("cancel");
    emit("close"); // 取消后默认关闭
  };

  // 确认按钮
  const handleConfirm = () => {
    emit("confirm");
    if (props.config.closeOnConfirm) {
      emit("close");
    }
  };
</script>

<style scoped>
  .dialog-mask {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: rgba(0, 0, 0, 0.5);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 9999;
  }

  .dialog-wrapper {
    width: 500px;
    background: #fff;
    border-radius: 8px;
    padding: 20px;
    position: relative;
  }

  .dialog-close {
    position: absolute;
    top: 15px;
    right: 15px;
    background: transparent;
    border: none;
    font-size: 20px;
    cursor: pointer;
  }

  .dialog-title {
    font-size: 18px;
    font-weight: bold;
    margin-bottom: 15px;
  }

  .dialog-content {
    min-height: 100px;
    margin-bottom: 20px;
  }

  .dialog-footer {
    display: flex;
    justify-content: flex-end;
    gap: 10px;
  }

  .dialog-btn {
    padding: 8px 16px;
    border: 1px solid #ccc;
    border-radius: 4px;
    cursor: pointer;
  }

  .dialog-btn.confirm {
    background: #409eff;
    color: white;
    border-color: #409eff;
  }
</style>

2. 命令式 API 封装(dialog.js

javascript
import { createVNode, render } from "vue";
import DialogContainer from "./DialogContainer.vue";

// 创建弹窗容器
const container = document.createElement("div");
document.body.appendChild(container);

// 存储当前弹窗实例(支持单例,如需多弹窗可改为数组)
let currentVNode = null;

export const dialog = {
  // 显示弹窗
  show(config = {}) {
    return new Promise((resolve, reject) => {
      // 关闭现有弹窗
      if (currentVNode) {
        this.close();
      }

      // 创建虚拟节点
      currentVNode = createVNode(DialogContainer, {
        config,
        // 绑定事件
        onConfirm: () => {
          resolve("confirm"); // 确认时resolve
        },
        onCancel: () => {
          reject("cancel"); // 取消时reject
        },
        onClose: () => {
          this.close(); // 关闭弹窗
        },
      });

      // 渲染到DOM
      render(currentVNode, container);
    });
  },

  // 关闭弹窗
  close() {
    if (currentVNode) {
      render(null, container); // 卸载组件
      currentVNode = null;
    }
  },
};

// 注册为Vue全局属性
export default {
  install(app) {
    app.config.globalProperties.$dialog = dialog;
  },
};

3. 注册到 Vue 应用

javascript
// main.js
import { createApp } from "vue";
import App from "./App.vue";
import dialogPlugin from "./dialog.js";

const app = createApp(App);
app.use(dialogPlugin); // 注册全局弹窗
app.mount("#app");

三、使用方式

1. 基础确认框

javascript
// 在组件中调用
async function openDefaultDialog() {
  try {
    await this.$dialog.show({
      title: "提示",
      content: "确定要删除这条数据吗?",
      confirmText: "删除",
      cancelText: "再想想",
    });
    // 确认后的逻辑(如删除数据)
    console.log("用户确认删除");
  } catch (e) {
    // 取消后的逻辑
    console.log("用户取消操作");
  }
}

2. 传入自定义组件

假设存在自定义组件 CustomForm.vue

vue
<template>
  <div>
    <input v-model="name" placeholder="请输入姓名" />
  </div>
</template>

<script setup>
  import { ref } from "vue";
  const name = ref("");
  // 暴露数据给父组件
  defineExpose({ name });
</script>

在弹窗中使用该组件:

javascript
import CustomForm from "./CustomForm.vue";

async function openCustomDialog() {
  try {
    // 存储自定义组件实例,用于获取内部数据
    let formInstance = null;

    await this.$dialog.show({
      title: "自定义表单",
      component: CustomForm,
      // 监听组件事件(如有)
      events: {
        // 假设组件有change事件
        change: (val) => console.log("表单变化:", val),
      },
      // 组件挂载后获取实例
      onMounted: (instance) => {
        formInstance = instance;
      },
    });

    // 确认后获取组件数据
    console.log("用户输入的姓名:", formInstance.name);
  } catch (e) {
    console.log("用户取消");
  }
}

3. 高级配置项

javascript
this.$dialog
  .show({
    title: "特殊配置",
    content: "点击遮罩不关闭,无关闭按钮",
    showClose: false, // 隐藏关闭按钮
    closeOnMask: false, // 点击遮罩不关闭
    hideFooter: false, // 显示底部按钮
    closeOnConfirm: false, // 确认后不关闭(如需手动处理关闭)
  })
  .then(() => {
    console.log("确认后不关闭,可手动处理");
    // 处理完逻辑后手动关闭
    this.$dialog.close();
  });

四、核心特性总结

  1. 默认确认框:支持标题、内容、按钮文本自定义,通过 Promise 处理确认/取消。
  2. 自定义组件:可传入任意 Vue 组件,支持传递 props 和事件,通过 defineExpose 获取组件内部数据。
  3. 灵活配置
    • showClose:是否显示关闭按钮
    • closeOnMask:点击遮罩是否关闭
    • hideFooter:是否隐藏底部按钮
    • closeOnConfirm:确认后是否自动关闭
    • 按钮文本自定义等

五、扩展方向

  • 多弹窗支持:将 currentVNode 改为数组,支持同时显示多个弹窗。
  • 动画效果:添加弹窗显示/隐藏的过渡动画(使用 Vue 的 <Transition> 组件)。
  • 样式定制:支持通过配置项传入自定义类名或样式。
  • 加载状态:在确认按钮添加加载状态,防止重复提交。

通过这种实现,既能保持命令式调用的简洁性,又能兼顾 Vue 组件的灵活性,满足大部分弹窗场景需求。

Released under the MIT License.